package com.example.demo.common.lbsgeo.geo;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by li on 2016/4/22.
 */
public class Geohash {
    // geohash's char map
    // no a's i's l's o's
    // old MacDonal wouldn't be happy
    private static char[] _base32 = {'0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',
            'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
    private final static Map _decodemap = new HashMap();
    static {
        int sz = _base32.length;
        for (int i = 0; i < sz; i++) {
            _decodemap.put(_base32[i], i);
        }
    }

    private static int precision = 12;
    private static int[] bits = {16, 8, 4, 2, 1};
    private final static String BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";
    private static final int TOP = 0;
    private static final int RIGHT = 1;
    private static final int BOTTOM = 2;
    private static final int LEFT = 3;
    private static final int EVEN = 0;
    private static final int ODD = 1;
    private static String[][] NEIGHBORS;
    private static String[][] BORDERS;

    static {
        NEIGHBORS = new String[2][4];
        BORDERS = new String[2][4];
        BORDERS[ODD][TOP] = "bcfguvyz";
        BORDERS[ODD][RIGHT] = "prxz";
        BORDERS[ODD][BOTTOM] = "0145hjnp";
        BORDERS[ODD][LEFT] = "028b";
        BORDERS[EVEN][TOP] = BORDERS[ODD][RIGHT];
        BORDERS[EVEN][RIGHT] = BORDERS[ODD][TOP];
        BORDERS[EVEN][BOTTOM] = BORDERS[ODD][LEFT];
        BORDERS[EVEN][LEFT] = BORDERS[ODD][BOTTOM];
        NEIGHBORS[ODD][TOP] = "238967debc01fg45kmstqrwxuvhjyznp";
        NEIGHBORS[ODD][RIGHT] = "14365h7k9dcfesgujnmqp0r2twvyx8zb";
        NEIGHBORS[ODD][BOTTOM] = "bc01fg45238967deuvhjyznpkmstqrwx";
        NEIGHBORS[ODD][LEFT] = "p0r21436x8zb9dcf5h7kjnmqesgutwvy";
        NEIGHBORS[EVEN][TOP] = NEIGHBORS[ODD][RIGHT];
        NEIGHBORS[EVEN][RIGHT] = NEIGHBORS[ODD][TOP];
        NEIGHBORS[EVEN][BOTTOM] = NEIGHBORS[ODD][LEFT];
        NEIGHBORS[EVEN][LEFT] = NEIGHBORS[ODD][BOTTOM];
    }

    public static void main(String[] args) {
        /*GeoHashKit ghf = new GeoHashKit();


        String gc1 = ghf.encode(31.277631, 120.53916300000003);
        String gc2 = ghf.encode(51.4797, -0.0124);

        System.out.println(gc1);
        System.out.println(gc2);

        double[] gd1 = ghf.decode(gc1);
        double[] gd2 = ghf.decode(gc2);
        System.out.println(gd1[0] + ", " + gd1[1]);
        System.out.println(gd2[0] + ", " + gd2[1]);

        System.out.println(ghf.encode(30.657356, 104.057447));
        System.out.println(ghf.encode(30.661371, 104.053026));
        //System.out.println(ghf.encode(30.657356, 104.057447));*/

    }


    public static String encode(double latitude, double longitude) {
        double[] lat_interval = {-90.0, 90.0};
        double[] lon_interval = {-180.0, 180.0};

        StringBuilder geohash = new StringBuilder();
        boolean is_even = true;
        int bit = 0, ch = 0;

        while (geohash.length() < precision) {
            double mid = 0.0;
            if (is_even) {
                mid = (lon_interval[0] + lon_interval[1]) / 2;
                if (longitude > mid) {
                    ch |= bits[bit];
                    lon_interval[0] = mid;
                } else {
                    lon_interval[1] = mid;
                }

            } else {
                mid = (lat_interval[0] + lat_interval[1]) / 2;
                if (latitude > mid) {
                    ch |= bits[bit];
                    lat_interval[0] = mid;
                } else {
                    lat_interval[1] = mid;
                }
            }

            is_even = is_even ? false : true;

            if (bit < 4) {
                bit++;
            } else {
                geohash.append(_base32[ch]);
                bit = 0;
                ch = 0;
            }
        }

        return geohash.toString();
    }

    public static double[] decode(String geohash) {
        double[] ge = decode_exactly(geohash);
        double lat, lon, lat_err, lon_err;
        lat = ge[0];
        lon = ge[1];
        lat_err = ge[2];
        lon_err = ge[3];

        double lat_precision = Math.max(1, Math.round(-Math.log10(lat_err))) - 1;
        double lon_precision = Math.max(1, Math.round(-Math.log10(lon_err))) - 1;

        lat = getPrecision(lat, lat_precision);
        lon = getPrecision(lon, lon_precision);

        return new double[]{lat, lon};
    }

    public static double[] decode_exactly(String geohash) {
        double[] lat_interval = {-90.0, 90.0};
        double[] lon_interval = {-180.0, 180.0};

        double lat_err = 90.0;
        double lon_err = 180.0;
        boolean is_even = true;
        int sz = geohash.length();
        int bsz = bits.length;
        double latitude, longitude;
        for (int i = 0; i < sz; i++) {

            Integer cd = (Integer) _decodemap.get(geohash.charAt(i));

            for (int z = 0; z < bsz; z++) {
                int mask = bits[z];
                if (is_even) {
                    lon_err /= 2;
                    if ((cd & mask) != 0) {
                        lon_interval[0] = (lon_interval[0] + lon_interval[1]) / 2;
                    } else {
                        lon_interval[1] = (lon_interval[0] + lon_interval[1]) / 2;
                    }

                } else {
                    lat_err /= 2;

                    if ((cd & mask) != 0) {
                        lat_interval[0] = (lat_interval[0] + lat_interval[1]) / 2;
                    } else {
                        lat_interval[1] = (lat_interval[0] + lat_interval[1]) / 2;
                    }
                }
                is_even = is_even ? false : true;
            }

        }
        latitude = (lat_interval[0] + lat_interval[1]) / 2;
        longitude = (lon_interval[0] + lon_interval[1]) / 2;

        return new double[]{latitude, longitude, lat_err, lon_err};
    }

    static double getPrecision(double x, double precision) {
        double base = Math.pow(10, -precision);
        double diff = x % base;
        return x - diff;
    }

    /**
     * 求与当前geohash相邻的8个格子的geohash值。
     *
     * @param geohash
     * @return string数组，周围格子的geohash值
     */
    public static String[] expand(String geohash) {

        String left = calculate(geohash, LEFT);
        String right = calculate(geohash, RIGHT);
        String top = calculate(geohash, TOP);
        String bottom = calculate(geohash, BOTTOM);

        String topLeft = calculate(top, LEFT);
        String topRight = calculate(top, RIGHT);
        String bottomLeft = calculate(bottom, LEFT);
        String bottomRight = calculate(bottom, RIGHT);

        return new String[]{topLeft, top, topRight, left, geohash, right, bottomLeft, bottom, bottomRight};
    }

    /**
     * 递归计算当前区域特定方向的geohash值
     *
     * @param geohash
     * @param direction 偏移方向
     * @return 周围区域的geohash值，超出边界则返回空字符串""
     */
    private static String calculate(String geohash, int direction) {
        if ("".equals(geohash))      //如果递归到第一个字符仍然处于边界，则不存在这一方向的相邻格子
            return "";
        int length = geohash.length();
        char lastChar = geohash.charAt(length - 1);
        int charType = (geohash.length() % 2) == 1 ? ODD : EVEN;  //最后一位是奇数还是偶数
        String base = geohash.substring(0, length - 1);
        if (BORDERS[charType][direction].indexOf(lastChar) != -1) { //判断对后一位是否处在边界
            base = calculate(base, direction);
        }
        if (!"".equals(base)) {
            return base + NEIGHBORS[charType][direction].charAt(BASE32.indexOf(lastChar));
        } else {
            return "";
        }
    }

    private final static double EARTH_RADIUS = 6378.137;
    private static double rad(double d)
    {
        return d * Math.PI / 180.0;
    }

    /**
     * 根据两点间经纬度坐标（double值），计算两点间距离，单位为米
     */
    public static double getDistance(double lat1, double lng1, double lat2, double lng2)
    {
        double radLat1 = rad(lat1);
        double radLat2 = rad(lat2);
        double a = radLat1 - radLat2;
        double b = rad(lng1) - rad(lng2);
        double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a/2),2) +
                Math.cos(radLat1)*Math.cos(radLat2)*Math.pow(Math.sin(b/2),2)));
        s = s * EARTH_RADIUS;
//        s = Math.round(s * 10000) / 10000;
        return s*1000;
    }
}
